/*
 * ============================================================================
 *                   GNU Lesser General Public License
 * ============================================================================
 *
 *
 *
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307, USA.
 *
 *
 *
 */
package rad.framework.jsf;

import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
import java.util.MissingResourceException;
import java.util.ResourceBundle;

import javax.faces.model.SelectItem;
import javax.persistence.Column;

import rad.framework.cache.ContextScopedCache;
import rad.framework.cache.ContextScopedCache.ApplicationScopeCache;
import rad.framework.cache.ContextScopedCache.SessionScopedCache;
import rad.framework.jpa.EntityHandle;
import rad.framework.jpa.EntityPersistenceContextMap;
import rad.framework.lang.ReflectionUtil;
import rad.framework.security.jpa.Action;
import rad.framework.security.jpa.PermissionsUtil;
import rad.framework.validator.Required;

import org.apache.commons.lang.StringUtils;
import org.hibernate.validator.Length;
import org.hibernate.validator.Max;
import org.hibernate.validator.Min;
import org.hibernate.validator.NotEmpty;
import org.hibernate.validator.NotNull;
import org.hibernate.validator.Range;
import org.jboss.seam.Component;
import org.jboss.seam.ScopeType;
import org.jboss.seam.annotations.Name;
import org.jboss.seam.annotations.Scope;

public class RadFrameworkFunctions {

	
	// --------------------------------------------------------------
	// Resource Bundle related
	// --------------------------------------------------------------

	public static Object rendered(Object instance, String property) {
		// TODO setup context variable - uncap simple name ??? - pdf - view-m2o
		return rule(instance, property, "rendered", Boolean.TRUE);
	}

	public static Object disabled(Object instance, String property) {
		// TODO setup context variable - uncap simple name ???
		// TODO @Id and @GeneratedValue
		return rule(instance, property, "disabled", Boolean.FALSE);
	}

	public static Object reRender(Object instance, String property) {
		return rule(instance, property, "reRender", null);
	}

	public static Object size(Object instance, String property) {
		return rule(instance, property, "size", null);
	}

	public static Object rows(Object instance, String property) {
		return rule(instance, property, "rows", "20");
	}

	public static Object cols(Object instance, String property) {
		return rule(instance, property, "cols", "90");
	}
	
	public static Object rule(Object instance, String property, String rule,
			Object defaultValue) {
		// generic
		if (instance == null) {
			return defaultValue;
		}
		Object value = getValue(instance.getClass().getSimpleName(), property,
				rule);
		
		if (value == null) {
			return defaultValue;
		}
		return value;
	}

	public static Object editable(Object instance) {
		// TODO ??? add to edit page restriction #{t:editable(entityName)}
		// TODO ??? hasEditPermission(instance)
		if (instance == null) {
			return Boolean.TRUE;
		}
		Object value = getValue(FunctionsEventScopedCache.instance().getBundle(), instance.getClass()
				.getSimpleName()
				+ ".editable");

		if (value != null) {
			return value;
		} else {
			return Boolean.TRUE;
		}
	}

	// --------------------------------------------------------------
	// Annotation/Reflection related
	// --------------------------------------------------------------

	public static boolean required(Object instance, String property) {
		Method method = getMethod(instance, property);

		if (method.isAnnotationPresent(Required.class)) {
			return true;
		}
		if (method.isAnnotationPresent(NotNull.class)) {
			return true;
		}
		if (method.isAnnotationPresent(NotEmpty.class)) {
			return true;
		}
		return false;
//		return (Boolean) rule(instance, property, "required", Boolean.FALSE);
	}

	public static Object length(Object instance, String property) {
		Method method = getMethod(instance, property);

		if (method.isAnnotationPresent(Length.class)) {
			return method.getAnnotation(Length.class).max();
		}
		if (method.isAnnotationPresent(Column.class)) {
			return method.getAnnotation(Column.class).length();
		}
		return "255";
	}

	public static Object min(Object instance, String property) {
		Method method = getMethod(instance, property);

		if (method.isAnnotationPresent(Min.class)) {
			return method.getAnnotation(Min.class).value();
		}
		if (method.isAnnotationPresent(Range.class)) {
			return method.getAnnotation(Range.class).min();
		}
		return "0";
	}

	public static Object max(Object instance, String property) {
		Method method = getMethod(instance, property);

		if (method.isAnnotationPresent(Max.class)) {
			return method.getAnnotation(Max.class).value();
		}
		if (method.isAnnotationPresent(Range.class)) {
			return method.getAnnotation(Range.class).max();
		}
		return "9999999";
	}

	public static List enumConstants(Object instance, String property) {
		String key = instance.getClass().getName() + "." + property + ".enum";

		SessionScopedCache enumCache = SessionScopedCache.instance();
		List<SelectItem> list = (List<SelectItem>) enumCache.get(key);
		if (list != null) {
			return list;
		}
		
		Method method = getMethod(instance, property);
		Class<?> clazz = method.getReturnType();
		
		list = new ArrayList<SelectItem>();
		String className = clazz.getSimpleName();
		ResourceBundle bundle = FunctionsEventScopedCache.instance().getBundle();
		Object[] constants = clazz.getEnumConstants();
		for (Object literal : constants) {
			String label = bundle.getString(className + "_" + literal);
			list.add(new SelectItem(literal, label));
		}
		
		enumCache.put(key, list);
		return list;
	}
	
	// --------------------------------------------------------------
	// Permission related
	// --------------------------------------------------------------

	public static boolean hasEditPermission(Object instance) {
		return PermissionsUtil.hasPermissions(instance, Action.UPDATE);
	}

	public static boolean hasDeletePermission(Object instance) {
		return PermissionsUtil.hasPermissions(instance, Action.DELETE);
	}
	
	// --------------------------------------------------------------
	// Persistence related
	// --------------------------------------------------------------
	
	public static Object find(String className, Object id) {
		String pcn = EntityPersistenceContextMap.instance().getPersistenceContextNameFor(className);
		Class<?> c = ReflectionUtil.forName(className);
		EntityHandle handle = new EntityHandle(pcn, c, id);
		return handle.find();
	}
	
	// --------------------------------------------------------------

	protected static Object getValue(String className,
			String property, String rule) {
		FunctionsEventScopedCache valueCache = FunctionsEventScopedCache.instance();
		String viewId = valueCache.getViewId();
		
		// view specific key
		String key = concat(viewId, className, property, rule);
		
		// check caches
		ApplicationScopeCache missCache = ApplicationScopeCache.instance();
		if (missCache.contains(key)) {
			return null;
		}
		Object value = valueCache.get(key);
		if (value != null) {
			return value;
		}
		
		ResourceBundle bundle = valueCache.getBundle();

		// view specific
		value = getValue(bundle, key);

		// class
		if (value == null) {
			value = getValue(bundle, concat(className, property, rule));
		}
		// TODO super class or too expensive ???
		
		// cache using view specific key
		if (value == null) {
			missCache.add(key);
		} else {
			valueCache.put(key, value);
		}
		return value;
	}

	protected static Object getValue(ResourceBundle bundle, String key) {
		try {
			// TODO ? http://www.seamframework.org/Community/HotRedeployOfMessageBundles
			Object value = bundle.getObject(key);
//			if (log.isDebugEnabled()) {
//				log.debug("Key: " + key + " Value: " + value);
//			}
			return value;
		} catch (MissingResourceException e) {
			return null;
		}
	}

	protected static String concat(String... values) {
		StringBuilder builder = new StringBuilder();
		for (int i = 0; i < values.length; i++) {
			if (values[i] != null) {
				builder.append(values[i]);
				if (i + 1 < values.length) {
					builder.append(".");
				}
			}
		}
		return builder.toString();
	}

	protected static Method getMethod(Object instance, String property) {
//		if (log.isDebugEnabled()) {
//			log.debug("Instance: " + instance + " Property: " + property);
//		}

		String key = instance.getClass().getName() + "." + property + ".method";

		// TODO is this faster ?
		ApplicationScopeCache cache = ApplicationScopeCache.instance();
		Method method = (Method) cache.get(key);
		if (method != null) {
			return method;
		}

		method = ReflectionUtil.findMethodByName(instance.getClass(),
				"get" + StringUtils.capitalize(property));
		if (method == null) {
			throw new RuntimeException("Getter Method Not Found: "
					+ instance.getClass().getSimpleName() + "." + property);
		}
		
		cache.put(key, method);
		return method;
	}
	
	//---------------------------------------------------------------
	// cache various results in either event, session or application scope
	// - The values might change on every request, but not across phases. Cache in event scope.
	// - Enums don't change but their local might. Cache in session scope.
	// - If the value isn't in a bundle then lets not continue to look. Cache in application scope.
	//---------------------------------------------------------------
	
	@Name("rad.framework.functionsEventCache")
	@Scope(ScopeType.EVENT)
	public static class FunctionsEventScopedCache extends ContextScopedCache {
		
		private ResourceBundle bundle = org.jboss.seam.core.ResourceBundle.instance();
		
		public ResourceBundle getBundle() {
			return bundle;
		}
		
		private String viewId;

		public String getViewId() {
			if (viewId == null) {
				// ex. jsf/MyEntity/View.xhtml = MyEntity.View 
				viewId = JsfUtil.getViewId();
				viewId = viewId.replace("/", ".");
//				viewId = viewId.substring(viewId.indexOf('.')+1);
				viewId = viewId.substring(viewId.indexOf('.')+1);
				viewId = viewId.substring(0, viewId.lastIndexOf('.'));
			}
			return viewId;
		}

		public static FunctionsEventScopedCache instance() {
			return (FunctionsEventScopedCache) Component.getInstance(FunctionsEventScopedCache.class);
		}
	}
}
